﻿using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text;
using System.Text.RegularExpressions;
using System.Net;
using System.Security.Cryptography;
using System.Configuration;


namespace CookieFreeAnalytics
{

    // Change Log
    // 25th April - UserAgent filter changed from "googlebot" to "google"
    // 4th July - Added "CollectorMode"
    // Version 1.0.2 - 20th Jan 2015 - All known bugs fixed.
    class Visitor
    {
        public string vBrowserFingerID { get; set; }
        public string vGeneratedVisitorID { get; set; }
        public long vFirstVisitTimeStamp { get; set; }
        public bool vReturningVisitor { get; set; }
        public int ReturningID { get; set; }

        public Visitor(string browserFingerID, string generatedid, long firstvisit, bool returningvisitor)
        {
            vBrowserFingerID = browserFingerID;
            vGeneratedVisitorID = generatedid;
            vFirstVisitTimeStamp = firstvisit;
            vReturningVisitor = returningvisitor;
            ReturningID = 1;
        }

        public bool ReturningVisitor(string ifmodifiedsince)
        {
            if (vReturningVisitor == false)
            {
                ReturningID = 2;
            }
            return true;
        }
    }

    public static class CFA_Core
    {
        #region Vars
        // IMPORTANT: Make sure your PropertyID is set to MO- not UA- in web.config otherwise some parameters (like referrer) won't work.        
        //private static string PropertyID = System.Configuration.ConfigurationManager.AppSettings["CookieFreePropertyID"];

        private const string utmVersion = "4.4sa"; // Provided by Google Mobile Web code
        private static string referrer = "-";
        private static string version = "1.0.2";

        //public static bool debugenabled;// = false; 
        private static Dictionary<string, string> browserFingerID = new Dictionary<string, string>();
        private static Dictionary<string, Visitor> VisitorInfo = new Dictionary<string, Visitor>();
        //public static int counter =0; // Not used

        #endregion

        /// <summary>
        /// TrackVisit() will update GA with current page request.
        /// Passing returnGif = true will not update GA.
        /// Return string contains url request sent to GA.
        /// </summary>
        /// 
        public static string TrackVisit(string virtualPageView = "")
        {

            bool sendToGA = true; // 4th July. 

            string sPath = System.Web.HttpContext.Current.Request.Url.AbsoluteUri;

            string sVisitorid = "";

            // Fetchfollow override to not send to GA at all if collection mode is on.
            if (CFAGlobal.EnableCollectionMode)
            {
                sendToGA = false;
            }

            //Block forward slash on its own, or treat as default.aspx?
            if ((HttpContext.Current.Request.RawUrl.Equals("/") && (!CFAGlobal.EnableAllowForwardSlash)) || (HttpContext.Current.Request.RawUrl.Contains("__utm.gif")))
            {
                // 4th March - Ignore collection mode so don't echo requests as / ends up in fetchfollow when dont want it.
                //if (CFAGlobal.EnableCollectionMode)
                //{
                //    sendToGA = false;
                //}
                //else
                //{
                //    return "";
                //}
                return "";
            }


            #region Start of Bot filtering. Check ECMA Script capable
            // New way of checking if the browser is Javascript capable


            if (HttpContext.Current.Request.Browser.EcmaScriptVersion.Major < 1)
            {
                // TO DO: Log user agent in database to check for false positives
                if (CFAGlobal.EnableCollectionMode)
                {
                    sendToGA = false;
                }
                else
                {
                    return "";
                }
                // We don't support Javascript so we aren't a browser //
            }
            #endregion

            // || UserAgent.indexof(spider) added 25th March
            string useragent = HttpContext.Current.Request.UserAgent;

            if ((useragent.IndexOf("spider", 0, StringComparison.CurrentCultureIgnoreCase) > 0) || (useragent.IndexOf("google", 0, StringComparison.CurrentCultureIgnoreCase) > 0))
            {
                if (CFAGlobal.EnableCollectionMode)
                {
                    sendToGA = false;
                }
                else
                {
                    return "";
                }
            }


            #region  IP Address Filter
            // Single IP tracking over ride. If enabled and IP matches return nothing
            if ((CFAGlobal.EnableIPFilter) && (HttpContext.Current.Request.UserHostAddress.Equals(CFAGlobal.ipfilter)))
            {
                if (CFAGlobal.EnableCollectionMode)
                {
                    sendToGA = false;
                }
                else
                {
                    return "";
                }
            }
            #endregion


            #region Folder Filter
            if (CFAGlobal.EnableFolderFilter)
            {
                if (CFAGlobal.IsFiltered(sPath))
                {

                    if (CFAGlobal.EnableCollectionMode)
                    {
                        sendToGA = false;
                    }
                    else
                    {
                        return "";
                    }
                }
            }
            #endregion

            // Check we have a .aspx page
            if (CFAGlobal.IsPageView(sPath))   // (sPath.ToLower().Contains(".aspx")) || (sPath.EndsWith("/")) || (sPath.EndsWith(".htm")) || (sPath.EndsWith(".html"))) // Added EndsWith to capture default home page with no extension on
            {
                // MASTER CODE
                //// Make sure we have a Session available for use
                //if ((HttpContext.Current != null) && (HttpContext.Current.Session != null))
                //{
                //    // Check to see if we have a VisitorID already stored in Session
                //    if (HttpContext.Current.Session["VisitorID"] != null)
                //    {
                //        sVisitorid = HttpContext.Current.Session["VisitorID"].ToString();
                //        //ResponseDebug("#5:" + sVisitorid +":#");
                //    }
                //    return UpdateGoogle(sVisitorid, true); // Pass true to allow the use of Session values

                //}
                //else
                //{
                //    return UpdateGoogle(GetgenVisitorIDfromBrowserFingerID(), false);

                //}
                // MASTER CODE

                // Call Update with Browser Fingerprint only, no session use

                return UpdateGoogle(GetgenVisitorIDfromBrowserFingerID(), false, sendToGA);



            }
            #region Tracking Call for PDF / ZIP
            // *******  Brute force way on how to track non aspx files. Better to use precondition="" for IIS7  *****//
            //      <system.webServer>
            //      <modules runAllManagedModulesForAllRequests="true"/>
            //      </system.webServer>
            // *******  Add the above to your web.config to be able to track files like pdf and zip  *****//
            if ((CFAGlobal.EnableEvents) && (CFAGlobal.IsEvent(sPath)))  //sPath.ToLower().Contains(".pdf") || sPath.ToLower().Contains(".zip"))
            {
                return UpdateGoogle(GetgenVisitorIDfromBrowserFingerID(), false, sendToGA); // Passing false stops Session from being used for non aspx files. These will show up as fresh visitor
                //return true;
            }
            #endregion

            return ""; // We didn't find an asset we wanted to track.
        }

        public static void TrackEvent(string eCategory = "", string eAction = "", string eLabel = "", int eValue = 0, bool eNonInteraction = false)
        {
            // This is a wrapper around the UpdateGoogle function
            UpdateGoogle(GetgenVisitorIDfromBrowserFingerID(), false, true, true, eCategory, eAction, eLabel, eValue, eNonInteraction);
        }

        // Track a page view by making a server side request to Google Analytics
        // Messy way to pass EventTracking. TODO: Make an array to pass extra utmXX thru as will need this for ecommerce later on.
        // Jan 31st - Added virtual page view parameter
        private static string UpdateGoogle(string sVisitorID, bool useSession, bool sendToGA, bool isEvent = false, string eCategory = "", string eAction = "", string eLabel = "", int eValue = 0, bool eNonInteraction = false, string virtualPageView = "")
        {
            string domainName = HttpContext.Current.Request.ServerVariables["SERVER_NAME"];
            string userAgent = HttpContext.Current.Request.UserAgent;

            // V1.0.0.3 //
            string requestRange = HttpContext.Current.Request.ServerVariables["HTTP_RANGE"]; // We want this to be empty so we only log the first hit
            if (requestRange != null)
            {
                return "";
            }



            //userAgent = "Mozilla/5.0+(Windows;+U;+Windows+NT+5.1;+en-gb;+SiteCon/8.8.14)";

            if ((userAgent == null) || (userAgent.Equals("-")))
            {
                userAgent = "";
            }

            // If client user agent contains the partial Monitoring agent then don't track
            if (CFAGlobal.EnableMonitoringFilter)
            {
                if (CFAGlobal.IsBlockedAgent(userAgent))
                {
                    return "";
                }
            }

            // Check for current Google Cookie. Might be best not to use GA cookie if ga.js still running.
            string currentUTM = "";
            if (CFAGlobal.EnableEatGACookieRtnVisit)
            {
                currentUTM = GetGoogleCookieUTMAA();
            }

            if (currentUTM.Equals("")) // No Google Cookie Found, empty ID
            {
                if ((sVisitorID == null) || (sVisitorID.Equals("-")) || (sVisitorID.Equals("")))
                {
                    sVisitorID = GenerateVisitorID(userAgent);
                }
            }
            else // We have a client side GA ID.
            {
                sVisitorID = currentUTM; // Start using the GA cookie ID
                VisitorIDfromBrowserFingerID(GenerateVisitorIDFingerPrint(), currentUTM); // Put GA ID in our cache
            }

            // V1.0.0.2 - Commented out this as we hard coded out the use of sessions
            //// Regardless of what ID we got put it in session now if available
            //if (useSession == true) // When useSession is false we are tracking PDF / ZIP which don't support Session
            //{
            //    if (HttpContext.Current.Session["VisitorID"] == null)
            //    {
            //        HttpContext.Current.Session["VisitorID"] = sVisitorID;
            //    }
            //}

            // Get referrer if it exists
            if (HttpContext.Current.Request.UrlReferrer != null && HttpContext.Current.Request.UrlReferrer.ToString() != "")
            {
                referrer = HttpContext.Current.Request.UrlReferrer.ToString();
                string http = "http://";
                if (HttpContext.Current.Request.IsSecureConnection == true) http = "https://";

                if (referrer.StartsWith(http + domainName))
                {
                    referrer = "0";
                }
            }


            string utmGifLocation;
            if (HttpContext.Current.Request.IsSecureConnection)
            {
                // In case we are showing the utm request, make sure its also on https
                utmGifLocation = "https://www.google-analytics.com/__utm.gif";
            }
            else
            {
                utmGifLocation = "http://www.google-analytics.com/__utm.gif";
            }

            // Added Jan 31st to cater for virtual page views
            string pageurl;
            if (virtualPageView.Length > 0)
            {
                pageurl = virtualPageView;
            }
            else
            {
                pageurl = HttpContext.Current.Request.Url.PathAndQuery; // NO LONGER CALL SQL FUNCTION CookieFreeAnalyticsDL.CFA_DL.GetPageURL(HttpContext.Current.Request.Url.PathAndQuery);
            }
            // V1.0.1 Experimental //
            string header = HttpContext.Current.Request.Headers["If-Modified-Since"];
            // Experimental //

            string extractGACookieInfo = GetGoogleCookieUTMInfo(domainName, sVisitorID);
            string browserencoding = "-";
            string userlang = "-";

            if ((HttpContext.Current.Request.Browser.PreferredResponseEncoding != "") && (HttpContext.Current.Request.Browser.PreferredResponseEncoding != null))
            {
                browserencoding = HttpContext.Current.Request.Browser.PreferredResponseEncoding.ToString();
            }

            if ((HttpContext.Current.Request.UserLanguages != null) && (HttpContext.Current.Request.UserLanguages.Length > 0))
            {
                String[] userLangarr = HttpContext.Current.Request.UserLanguages;
                userlang = userLangarr[0].ToString();
            }


            // This section based on Google Mobile code //
            // Construct the gif hit url.
            string utmUrl = utmGifLocation + "?" +
                "utmwv=" + utmVersion +
                "&utmn=" + RandomNumber() +
                "&utmhn=" + HttpUtility.UrlEncode(domainName) +
                "&utmr=" + HttpUtility.UrlEncode(referrer) +
                "&utmp=" + HttpUtility.UrlEncode(pageurl) +
                "&utmul=" + userlang + // User Lang 
                "&utmac=" + CFAGlobal.PropertyID +
                "&utmcc=__utma%3D" + extractGACookieInfo + "%3B" +
                "&utmvid=" + GADomainHash(sVisitorID); // sVisitorID;


            //bool isEvent = false, string eCategory = "",string eAction = "",string eLabel = "",int eValue = 0,bool eNonInteraction = false
            //utme=5(Videos*Play*Babys%20First%20Birthday5)(98)
            if (isEvent)
            {
                utmUrl += "&utmt=event&utme=5(" + eCategory + "*" + eAction + "*" + eLabel + ")(" + eValue.ToString() + ")";
            }



            // == true when server side call not needed. Just return image url
            if (CFAGlobal.EnableGIFTracking == true)
            {
                return utmUrl;
            }

            // Now we have the full url, make the call
            try
            {
                // Based on Google Mobile tracking code //
                // if sendToGA is true then send request as we haven't triggered any filters.
                // if sendToGA is false, ie we have a spider or filtered folder then dont sent to GA but continue incase EnableExporting is enabled
                if (sendToGA)
                {
                    WebRequest connection = WebRequest.Create(utmUrl);

                    ((HttpWebRequest)connection).UserAgent = HttpContext.Current.Request.UserAgent;
                    connection.Headers.Add("Accept-Language", HttpContext.Current.Request.Headers.Get("Accept-Language"));
                    connection.Timeout = 1000; // In the unlikely melt down of GA we don't want to wait endlessly to time out

                    using (WebResponse resp = connection.GetResponse())
                    {
                        // Ignore response
                        // We have now make a tracking request for this request.
                    }
                }

                # region Exporting GA Requests
                // V1.0.0.2 //
                // If we have enabled exporting to the "Collector" website, echo request there.
                // If we have enabled additional "CollectionMode", we can also track spiders
                if (CFAGlobal.EnableExporting)
                {
                    // Swap the Google Gif url with our Export url, but leave query string intact
                    utmUrl = utmUrl.Replace(utmGifLocation, CFAGlobal.exportURL);


                    WebRequest connectionExport = WebRequest.Create(utmUrl);
                    ((HttpWebRequest)connectionExport).UserAgent = HttpContext.Current.Request.UserAgent;


                    connectionExport.Headers.Add("Accept-Language", HttpContext.Current.Request.Headers.Get("Accept-Language"));

                    // Custom HTTP headers our Collection service will be expecting. These aren't added to GA requests.
                    connectionExport.Headers.Add("ClientIP", HttpContext.Current.Request.UserHostAddress); // Add our own header of the visitor IP address
                    connectionExport.Headers.Add("VisitorID", sVisitorID); // Add our own header of the visitor id



                    // Added a short time out as we only need to make the connection and not worry about any processing overhead on reply.
                    connectionExport.Timeout = 1000;

                    using (WebResponse resp = connectionExport.GetResponse())
                    {
                        // Ignore response
                        // We have now make a tracking request for this request.
                    }
                }
                // END V1.0.0.2 //
                # endregion

                # region Debug Requests
                // In debug mode we call ourselves
                if (CFAGlobal.debugenabled)
                {
                    utmUrl = utmUrl.Replace("www.google-analytics.com", domainName);
                    WebRequest connectionDebug = WebRequest.Create(utmUrl);

                    ((HttpWebRequest)connectionDebug).UserAgent = HttpContext.Current.Request.UserAgent;
                    connectionDebug.Headers.Add("Accept-Language", HttpContext.Current.Request.Headers.Get("Accept-Language"));

                    using (WebResponse resp = connectionDebug.GetResponse())
                    {
                        // Ignore response
                        // We have now make a tracking request for this request.
                    }


                }
                # endregion
                return utmUrl;
            }
            catch (Exception ex)
            {

                return ""; // Return empty string if it didnt work
            }


        }



        #region Helper Functions

        // Generate a visitor id for this hit.
        private static string GenerateVisitorID(string userAgent)//, string cookie)
        {
            String toMD5 = Guid.NewGuid().ToString() + RandomNumber() + userAgent;
            MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
            byte[] inputBytes = Encoding.UTF8.GetBytes(toMD5);
            byte[] hashBytes = md5.ComputeHash(inputBytes);

            string md5hash = BitConverter.ToString(hashBytes);
            md5hash = md5hash.Replace("-", "");
            md5hash = md5hash.PadLeft(32, '0');
            md5hash = "0x" + md5hash.Substring(0, 16);
            return VisitorIDfromBrowserFingerID(GenerateVisitorIDFingerPrint(), md5hash);
        }

        // Create a very crude finger print for current visitor
        // Only used for places a normal session isn't accessible
        private static string GenerateVisitorIDFingerPrint()
        {
            string userAgent = HttpContext.Current.Request.UserAgent;
            string clientIP = HttpContext.Current.Request.UserHostAddress;

            String toMD5 = userAgent + clientIP;
            MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
            byte[] inputBytes = Encoding.UTF8.GetBytes(toMD5);
            byte[] hashBytes = md5.ComputeHash(inputBytes);

            string md5hash = BitConverter.ToString(hashBytes);
            md5hash = md5hash.Replace("-", "");
            md5hash = md5hash.PadLeft(32, '0');
            md5hash = "0x" + md5hash.Substring(0, 16);

            return md5hash.Substring(0, 16);
        }



        private static String RandomNumber()
        {
            Random rndNumber = new Random();
            return rndNumber.Next(2147483647).ToString();
        }

        private static void ResponseDebug(string output)
        {
            if (CFAGlobal.debugenabled)
            {
                //HttpContext.Current.Response.Write("<!-- " + output + "-->");
                HttpContext.Current.Response.Write(output);
            }
        }


        private static string VisitorIDfromBrowserFingerID(string browserFinger, string genVisitorID)
        {
            string returnvalue;
            // If we don't have the ID already saved then store it away
            if (browserFingerID.ContainsKey(browserFinger) == false)
            {
                browserFingerID[browserFinger] = genVisitorID;

                return genVisitorID;
            }
            else // We already have the ID, return that based on browser finger print
            {
                if (browserFingerID.TryGetValue(browserFinger, out returnvalue))
                {
                    return returnvalue;
                }
                return genVisitorID;
            }
        }

        private static string GetgenVisitorIDfromBrowserFingerID()
        {
            string browserFinger = GenerateVisitorIDFingerPrint();
            string returnvalue;
            try
            {
                // If we have the ID then return the previous sVisitorID
                if (browserFingerID.TryGetValue(browserFinger, out returnvalue))
                {
                    return returnvalue;
                }
                else // We have nothing so return the origional string
                {
                    // This is where we want to check to see if its the first hit for this finger print
                    // if it is then set the time stamp

                    // Set it so we know we only run this once
                    browserFingerID[browserFinger] = browserFinger;

                    Visitor tmpInfo = new Visitor(browserFinger, "", GetEpochTime(), false);

                    VisitorInfo[browserFinger] = tmpInfo;

                    return browserFinger;
                }
            }
            catch (Exception)
            {

                return browserFinger;
            }

        }

        private static string GetGoogleCookieUTMAA()
        {
            HttpCookie utmaa = new HttpCookie("__utma");
            utmaa = HttpContext.Current.Request.Cookies["__utma"];

            // Read the cookie information and display it.
            if (utmaa != null)
            {
                //ResponseDebug("#AvailableGA#"+GetGoogleCookieVisitorID(utmaa.Value,0) + "#A#");
                return GetGoogleCookieVisitorID(utmaa.Value, 0);
            }
            else
            {
                return "";
            }
        }


        private static string GetGoogleCookieUTMInfo(string domain, string sVisitorID)
        {
            HttpCookie utmaa = new HttpCookie("__utma");
            utmaa = HttpContext.Current.Request.Cookies["__utma"];

            if (CFAGlobal.EnableEatGACookieRtnVisit)
            {
                // Read the cookie information and display it.
                if (utmaa != null)
                {
                    string cookieextract = GetGoogleCookieVisitorID(utmaa.Value, 0) + "." + GetGoogleCookieVisitorID(utmaa.Value, 1) +
                         "." + GetGoogleCookieVisitorID(utmaa.Value, 2) + "." + GetGoogleCookieVisitorID(utmaa.Value, 3) +
                         "." + GetEpochTime()
                          + "." + IncreaseSessionCount(GetGoogleCookieVisitorID(utmaa.Value, 5));

                    //ResponseDebug("#A#" + cookieextract);
                    return cookieextract;
                }
                else
                {
                    // Changed March 23rd
                    // We have no google cookie so return the default code
                    return "999.999.999.999.999.1";
                }
            }
            else
            {
                // V1.0.1 Experimental //
                // Seems only Chrome always uses If-Modified-Since
                string header = HttpContext.Current.Request.Headers["If-Modified-Since"];
                DateTime dt;
                if (DateTime.TryParse(header, out dt))
                {
                    TimeSpan t = (dt - new DateTime(1970, 1, 1).ToLocalTime());
                    int timestamp = ((int)t.TotalSeconds) - 3600; // 3600 is one hour in seconds. GMT fudge
                    if (timestamp < VisitorInfo[sVisitorID].vFirstVisitTimeStamp)
                    {
                        VisitorInfo[sVisitorID].vReturningVisitor = true;
                        VisitorInfo[sVisitorID].ReturningID = 2;
                    }
                    ResponseDebug("C: " + timestamp + "<br/>1st: " + VisitorInfo[sVisitorID].vFirstVisitTimeStamp);

                }

                // V1.0.1 Experimental //

                // Return standard UTMA value string from example.
                return "999.999.999.999.999." + VisitorInfo[sVisitorID].ReturningID;
            }
        }


        // Used to split up utma cookie
        private static string GetGoogleCookieVisitorID(string utmaa, int position)
        {
            string[] utmaacookie = utmaa.Split('.');
            return utmaacookie[position];
        }

        // Used to handle GA time stamps;
        private static long GetEpochTime()
        {
            TimeSpan t = (DateTime.UtcNow - new DateTime(1970, 1, 1).ToLocalTime());
            int timestamp = (int)t.TotalSeconds;
            return timestamp;
        }

        // Sessions last 30 minutes. If we are over 30 mins since time stamp start a new session
        private static int IncreaseSessionCount(string oldsession)
        {

            int sess = int.Parse(oldsession);

            if ((GetEpochTime() - long.Parse(oldsession)) >= 1800)
                sess++;

            return sess;
        }

        // Use this to clear up sessions. Should add a call in Session_End
        private static void CleanupSessions(string sessionID)
        {
            // Not yet implemented. Is it needed if app pool recycles every few hours?
        }


        private static int GADomainHash(string d)
        {


            d = d.ToLower();
            if (d.StartsWith("www."))
            {
                d = d.Replace("www.", "");
            }


            int a = 1;
            int c = 0;
            int h;
            int o;

            if (d.Length > 0)
            {
                a = 0;
                for (h = (d.Length) - 1; h >= 0; h--)
                {
                    o = (Int32)d[h];
                    a = (a << 6 & 268435455) + o + (o << 14);
                    c = a & 266338304;
                    a = c != 0 ? a ^ c >> 21 : a;
                }
            }
            return a;
        }

        #endregion
    }

}